﻿// -----------------------------------------------------------------------
// <copyright file="PngWriter.cs">
// Copyright (c) 2013 Andrey Veselov. All rights reserved.
// License:  Microsoft Public License (MS-PL)
// Contacts: http://andrey.moveax.com  andrey@moveax.com
// </copyright>
// -----------------------------------------------------------------------

namespace PortableImagingLibrary.Png
{
    using System;
    using System.IO;
    using System.Threading.Tasks;
    using PortableImagingLibrary.AsyncSupport;
    using PortableImagingLibrary.Convertors;
    using PortableImagingLibrary.Exceptions;
    using PortableImagingLibrary.Png.Compressors;
    using PortableImagingLibrary.Png.Crc;
    using PortableImagingLibrary.Png.DataStructures;
    using PortableImagingLibrary.Png.Filters;

    internal class PngWriter
    {
        private readonly byte[] _signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
        private const int _headerChunkDataSizeInBytes = 13;
        private const string _headerChunkTypeName = "IHDR";

        /// <summary>Saves image into the specified output stream. </summary>
        /// <param name="image">Source image</param>
        /// <param name="target">The output stream.</param>
        /// <param name="options">The save options</param>
        public async Task SaveAsync(Image image, Stream target, PngSaveOptions options)
        {
            using (var writer = new BinaryWriterAsync(target)) {
                // Write signature
                await writer.WriteBytesAsync(this._signature).ConfigureAwait(false);

                // Write header chunk                
                var header = new PngHeader() {
                    Width = image.Width,
                    Height = image.Height,
                    BitDepth = options.BitDepth,
                    ColorType = options.ColorType,
                    CompressionType = CompressionType.Deflate,
                    FilterType = FilterType.AdaptiveFiltering,
                    InterlaceType = InterlaceType.Standard
                };

                var crc = CrcFactory.Create();

                var headerData = this.BuildHeaderChunkData(header);
                await this.WriteChunkAsync(PngWriter._headerChunkTypeName, headerData, writer, crc).ConfigureAwait(false);

                var bkgndData = this.BuildBackgroundColorData(image, header);
                await this.WriteChunkAsync("bKGD", bkgndData, writer, crc).ConfigureAwait(false);

                using (var compressor = CompressorFactory.CreateCompressor(CompressionType.Deflate)) {
                    var filter = FilterFactory.CreateFilter(header.FilterType);

                    var data = this.BuildImageDataChunk(image, header, filter, compressor);
                    await this.WriteChunkAsync("IDAT", data, writer, crc).ConfigureAwait(false);
                }

                // Write end chunk
                await this.WriteChunkAsync("IEND", null, writer, crc).ConfigureAwait(false);
            }
        }

        #region Chunk writer methods

        private async Task WriteChunkAsync(string chunkTypeName, byte[] data, BinaryWriterAsync writer, ICrc crc)
        {
            if (chunkTypeName.Length != 4)
                throw new InvalidImageHeaderException(string.Format("Invalid chunk type name: {0}", chunkTypeName));

            int dataLength = data != null ? data.Length : 0;

            await writer.WriteIntAsync(dataLength);
            await writer.WriteStringAsync(chunkTypeName);

            if (data != null)
                await writer.WriteBytesAsync(data);

            // Get CRC
            crc.Reset();
            crc.Update(chunkTypeName);

            if (data != null)
                crc.Update(data);

            await writer.WriteUintAsync(crc.CurrentValue);
        }

        private byte[] BuildHeaderChunkData(PngHeader header)
        {
            var data = new byte[PngWriter._headerChunkDataSizeInBytes];

            var widthArray = BitConverter.GetBytes(header.Width);
            Array.Reverse(widthArray);
            Array.Copy(widthArray, 0, data, 0, widthArray.Length);

            var heightArray = BitConverter.GetBytes(header.Height);
            Array.Reverse(heightArray);
            Array.Copy(heightArray, 0, data, 4, widthArray.Length);

            data[08] = header.BitDepth;
            data[09] = (byte)header.ColorType;
            data[10] = (byte)header.CompressionType;
            data[11] = (byte)header.FilterType;
            data[12] = (byte)header.InterlaceType;

            return data;
        }

        private byte[] BuildImageDataChunk(Image image, PngHeader header, IFilter filter, ICompressor compressor)
        {
            int elementsPerSample = header.ColorType.ElementsPerSample();
            var currentLine = new byte[elementsPerSample * header.Width + 1]; // 1 byte for control code
            var pixels = image.Pixels;

            // Write image
            for (int lineIndex = 0; lineIndex < header.Height; lineIndex++) {
                // Convert current line into byte-array
                for (int x = 0; x < header.Width; x++) {
                    uint pixel = pixels[lineIndex * header.Width + x];
                    int pos = 1 + x * elementsPerSample;

                    currentLine[pos + 3] = ByteConvertor.GetByte(pixel, 3); // Alpha
                    currentLine[pos + 0] = ByteConvertor.GetByte(pixel, 2); // Red
                    currentLine[pos + 1] = ByteConvertor.GetByte(pixel, 1); // Green
                    currentLine[pos + 2] = ByteConvertor.GetByte(pixel, 0); // Blue 
                }

                filter.FilterLine(currentLine, (byte)elementsPerSample);
                compressor.AddLine(currentLine);
            }

            var data = compressor.GetChunkData();
            return data;
        }

        private byte[] BuildBackgroundColorData(Image image, PngHeader header)
        {
            switch (header.ColorType) {
                case ColorType.Greyscale:
                case ColorType.GreyscaleWithAlpha:
                    throw new NotSupportedException();

                case ColorType.Truecolor:
                case ColorType.TruecolorWithAlpha:
                    return ColorConvertor.Convert8BitColorTo16BitArray(image.BackgroundColor);

                case ColorType.IndexedColor:
                    throw new NotImplementedException();

                default:
                    throw new InvalidImageFormatException("invalid colorType");
            }
        }

        #endregion Chunk writer methods
    }
}